package com.google.code.stalkernet.database;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import com.google.code.stalkernet.entry.Entry;
import com.google.code.stalkernet.entry.EntryBody;
import com.google.code.stalkernet.entry.EntryID;
import com.google.code.stalkernet.entry.EntryUtils;

/**
 * Simple in-memory implementation of {@link Database} interface.
 *
 * @author Mikhail Vladimirov
 */
public class InMemoryDatabase extends AbstractDatabase
{
    private final static Random random = new Random ();

    private final Map <EntryID, Entry> entries = new HashMap <> ();
    private final Set <EntryID> leafIDs = new HashSet <> ();
    private final List <EntryID> entryIDs = new ArrayList <> ();

    /**
     * {@inheritDoc}
     */
    @Override
    public EntryID [] getLeafIDs ()
    {
        return leafIDs.toArray (new EntryID [leafIDs.size ()]);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasEntry (EntryID entryID)
    {
        if (entryID == null)
            throw new IllegalArgumentException ("Entry ID is null");

        return entries.containsKey (entryID);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Entry getEntry (EntryID entryID) throws NoSuchEntryException
    {
        if (entryID == null)
            throw new IllegalArgumentException ("Entry ID is null");

        Entry entry = entries.get (entryID);

        if (entry == null)
            throw new NoSuchEntryException (
                "No entry with such ID: " + entryID);

        return entry;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addEntry (Entry entry) throws EntryAlreadyExistsException,
        NoSuchEntryException
    {
        if (entry == null)
            throw new IllegalArgumentException ("Entry is null");

        EntryID entryID = entry.getID ();
        if (entries.containsKey (entryID))
            throw new EntryAlreadyExistsException (
                "Entry with such ID already exists: " + entryID);

        EntryID [] parentIDs = entry.getParentIDs ();

        for (EntryID parentID: parentIDs)
        {
            if (!entries.containsKey (parentID))
                throw new NoSuchEntryException (
                    "No entry with such ID: " + parentID);
        }

        entries.put (entryID, entry);
        leafIDs.removeAll (Arrays.asList (parentIDs));
        leafIDs.add (entryID);

        entryIDs.add (entryID);

        fireEntryAdded (entry);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Entry createEntry (EntryBody entryBody)
    {
        if (entryBody == null)
            throw new IllegalArgumentException ("Entry body is null");

        EntryID [] parentIDs = chooseParents ();

        byte [] bytes = new byte [16];
        random.nextBytes (bytes);

        return EntryUtils.createEntry (
            EntryUtils.createEntryID (bytes), entryBody, parentIDs);
    }

    private EntryID [] chooseParents ()
    {
        Set <EntryID> parentIDs = new HashSet<> (
            Arrays.asList (getLeafIDs ()));

        int count = entryIDs.size ();

        if (count > 0 && parentIDs.size () < 4)
            for (int i = 0; i < 16 && parentIDs.size () < 4; i++)
                parentIDs.add (entryIDs.get (random.nextInt (count)));

        return parentIDs.toArray (new EntryID [parentIDs.size ()]);
    }
}
